iT邦幫忙

2022 iThome 鐵人賽

DAY 4
0

環保當道,不過你可能不知道,Python 其實也很環保!今天咱們來認識一下 Python 的資源回收機制。
在此之前,先來理解一下 variable reference

Variable reference

我們先指派一個變數 num_1 等於 整數 10:

num_1 = 10
id(num_1)
4376314384

這短短一行變數宣告做了什麼? Python 萬物皆物件,於是記憶體中產生了一個整數10物件,變數名稱 num_1 則像是一個便利貼,指到該整數的記憶體位置。
https://ithelp.ithome.com.tw/upload/images/20220915/20151657KFkRkffS42.png

我們再指派一個變數 num_2 = num_1:

num_2 = num_1
id(num_2)
4376314384

https://ithelp.ithome.com.tw/upload/images/20220915/20151657GoTvz6nfGl.png

num_2 = num_1,即是 num_1 將其參考的記憶體位置 4376314384 傳給 num_2,讓兩個變數指到相同的記憶體位置。那麼,在 Python 中,有沒有方法可以找出物件目前被參考幾次呢?

有內建函式可以辦到:

import sys, ctypes

lst = [1,2,3]
print(sys.getrefcount(lst))
print(ctypes.c_long.from_address(id(lst)).value)
2
1

如上,有兩個函式可以得知物件目前被參考了幾次。為什麼第一個函式 sys.getrefcount 得出的值多了1呢?因為在把變數lst當成引數傳入時,又被參考了一次,所以多加了1。從這個角度來看,第二個函式 ctypes.c_long.from_address 因為是傳物件的 id 進去,可以得到正確的參考值。推薦使用第二個函式,用第一個的話則要記得自行減1歐!

至於為什麼不用我們一開始的整數10當上面程式的範例,而要用個 list [123] 啊?這就留待明天解釋吧!
(好奇的話可以自行試試看用,會有奇怪的事發生

Garbage collection

有一種變數參考的情況稱為 circular reference :物件A和物件B同時互相參考,比如以下的 class:

class A:
    def __init__(self):
        self.B = B(self)

class B:
    def __init__(self, A):
        self.A = A
# init 一個 A 物件,觀察有沒有 circular references.
var_A = A()
print(id(var_A), id(var_A.B))
print(id(var_A.B), id(var_A.B.A))
4565225424 4564263280
4564263280 4565225424

發現了嗎?A物件和B物件彼此是對方的 property 因而形成 circular reference, id 互相對應!
這時候砍掉變數 var_A 會發生什麼情況?

# gc 是 garbage collection 的縮寫,Python 的空間(記憶體)管理機制
# 我們先寫一個檢查物件還在不在記憶體的函式
import gc

def check_obj_exist(obj_id):
    for i in gc.get_objects():
        if id(i) == obj_id:
            return '物件還在'
    return '物件不在了'

# 寫一個檢查物件參考數量的函式備用
import ctypes

def check_ref_counts(obj_id):
    return ctypes.c_long.from_address(obj_id).value

print('A物件參考數量:', check_ref_counts(id(var_A)))
print(check_obj_exist(id(var_A)))
print('B物件參考數量:', check_ref_counts(id(var_A.B)))
print(check_obj_exist(id(var_A.B)))
id_var_A = id(var_A)    # 先把 var_A 的 ID 存起來, 刪掉後就拿不到了
id_var_B = id(var_A.B)
# 關掉 garbage collection 機制,會花生省魔術?
gc.disable()            # Disable garbage collection.
del var_A

A物件參考數量: 2
物件還在
B物件參考數量: 1
物件還在
# var_A 被刪除以後⋯⋯
print('ref to object A after var_A set to None:', check_ref_counts(id_var_A))
print(check_obj_exist(id_var_A))

print('ref to object B after var_B set to None:', check_ref_counts(id_var_B))
print(check_obj_exist(id_var_B))
ref to object A after var_A set to None: 1
物件還在
ref to object B after var_B set to None: 1
物件還在

什麼!明明A物件「唯一」的參考 var_A 刪掉了,A物件卻還在記憶體上,因為B物件其實仍在參考它,是 circular reference 的無限循環。
這個永遠刪不掉的A物件(和B物件)就會成為 memory leakage

讓我們再次把 garbage collection 打開:

gc.collect()
print(check_obj_exist(id_var_A))
print(check_obj_exist(id_var_B))
物件不在了
物件不在了

AB兩個物件都消失了! Python 的 garbage collection 會自動處理這種無限循環 reference 的情況,真是太好了!這讓我們寫程式少了很多BUG呀⋯⋯因為寫出 circular reference 其實比想像中容易很多/images/emoticon/emoticon16.gif

但還沒完!如果我們繼續針對A、B兩個物件的ID檢查 reference 數量的話⋯⋯
多執行幾次下面的程式碼,奇怪的事情會發生!!!

print('check the var_A ref counts again:', check_ref_counts(id_var_A))
print(check_obj_exist(id_var_A))
print('check the var_B ref counts again:', check_ref_counts(id_var_B))
print(check_obj_exist(id_var_B))
check the var_A ref counts again: 0
物件不在了
check the var_B ref counts again: 8589934594
物件不在了

B物件已經不在了,但它的ID竟然有 8589934594 參考???

這是因為,該ID位址的B物件的確已經刪除,但現在這個記憶體位址上有什麼?沒人知道!也許什麼都沒有,也許是一堆垃圾,每次執行結果可能都不同,

結論: 直接對記憶體位址本身操作並不安全!

除非有好理由(比如 debug 記憶體問題),不然要避免直接使用記憶體位址歐!

我們明天見~~~

參考:Python 3: Deep Dive (Part 1 - Functional)


上一篇
Class 玩玩看:寫一個長方形
下一篇
Python內建的提升效能機制(一)
系列文
小青蛇變大蟒蛇——進階Python學起來!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言